#region License, Terms and Conditions
//
// Prawks - Copyright (c) 2007 Atif Aziz. All rights reserved.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 2.1 of the License, or (at your option)
// any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
//
#endregion

namespace Prawks.SqlMethods
{
    using System;
    using System.Data;
    using System.Data.SqlClient;
    using Prawks.DbMethods;
    using CodeProvisioning;

    using MethodInfo = System.Reflection.MethodInfo;
    using ParameterInfo = System.Reflection.ParameterInfo;
    using Debug = System.Diagnostics.Debug;
    using IDeserializationCallback = System.Runtime.Serialization.IDeserializationCallback;

    /// <summary>
    /// Supplies the implementation for a method that has been marked with
    /// the attribute <see cref="SqlMethodAttribute"/>. This class may not be
    /// inherited.
    /// </summary>

    [ Serializable ]
    public sealed class SqlMethod : LogicalMethodImpl, IDbMethod, IDeserializationCallback
    {
        [ NonSerialized ] private SqlCommand _command;
        [ NonSerialized ] private int _inParamsCount;

        /// <summary>
        /// Initializes a new instance of the <see cref="SqlMethod"/> class with
        /// the method for which it provides the implementation.
        /// </summary>
        /// <param name="method">
        /// The method to which this instance is bound.
        /// </param>
        
        public SqlMethod(MethodInfo method) : base(method) 
        {
            Initialize();
        }

        internal SqlMethod(MethodInfo method, SqlMethodAttribute attribute) 
            : base(method)
        {
            Initialize(attribute);
        }

        void IDeserializationCallback.OnDeserialization(object sender)
        {
            Initialize();
        }

        /// <summary>
        /// Creates a <see cref="SqlCommand"/> object based on the signature of the bound method.
        /// </summary>
        /// <returns>
        /// Returns the new <see cref="SqlCommand"/> object. The values of the
        /// command's parameters are not initialized.
        /// </returns>

        public SqlCommand CreateCommand()
        {
            return CreateCommand(null, null, null);
        }

        /// <summary>
        /// Creates a <see cref="SqlCommand"/> object based on the signature of the bound method.
        /// In addition, the command object is associated with a given
        /// <see cref="SqlConnection"/> object.
        /// </summary>
        /// <param name="connection">
        /// The <see cref="SqlConnection"/> object to set the 
        /// <see cref="SqlCommand.Connection"/> of the new command. This may be
        /// a null reference in which case the command is not connected.
        /// </param>
        /// <returns>
        /// Returns the new <see cref="SqlCommand"/> object. The values of the
        /// command's parameters are not initialized.
        /// </returns>

        public SqlCommand CreateCommand(SqlConnection connection)
        {
            return CreateCommand(connection, null, null);
        }

        /// <summary>
        /// Creates a <see cref="SqlCommand"/> object based on the signature of the bound method.
        /// In addition, the command object is associated with a <see cref="SqlConnection"/> 
        /// and its parameter values are initialized with a given array of values.
        /// </summary>
        /// <param name="connection">
        /// The <see cref="SqlConnection"/> object to set the 
        /// <see cref="SqlCommand.Connection"/> of the new command to. This may be
        /// a null reference in which case the command is not connected.
        /// </param>
        /// <param name="values">
        /// An array of values that are used to initialize the parameters of
        /// the new command. The order the values in the array must be the same
        /// as those of the <see cref="IMethodImpl.Method"/>'s parameters. This may be a
        /// null reference, in which case the command parameter values are
        /// uninitialized.
        /// </param>
        /// <returns>
        /// Returns the new <see cref="SqlCommand"/> object. The values of the
        /// command's parameters are not initialized.
        /// </returns>

        public SqlCommand CreateCommand(SqlConnection connection, object[] values)
        {
            return CreateCommand(connection, values, null);
        }

        /// <summary>
        /// Creates a <see cref="SqlCommand"/> object based on the signature of the bound method.
        /// In addition, the command object is associated with a <see cref="SqlConnection"/>, 
        /// its parameter values are initialized with a given array of values. The command
        /// is also bound to a <see cref="SqlTransaction"/>.
        /// </summary>
        /// <param name="connection">
        /// The <see cref="SqlConnection"/> object to set the 
        /// <see cref="SqlCommand.Connection"/> of the new command to. This may be
        /// a null reference in which case the command is not connected.
        /// </param>
        /// <param name="values">
        /// An array of values that are used to initialize the parameters of
        /// the new command. The order the values in the array must be the same
        /// as those of the <see cref="IMethodImpl.Method"/>'s parameters. This may be a
        /// null reference, in which case the command parameter values are
        /// uninitialized.
        /// </param>
        /// <param name="transaction">
        /// The <see cref="SqlTransaction"/> object to set the 
        /// <see cref="SqlCommand.Transaction"/> of the new command to. This may be
        /// a null reference in which case the command will not run under a
        /// transaction.
        /// </param>
        /// <returns>
        /// Returns the new <see cref="SqlCommand"/> object. The values of the
        /// command's parameters are not initialized.
        /// </returns>

        public SqlCommand CreateCommand(SqlConnection connection, object[] values, 
            SqlTransaction transaction)
        {
            if (values != null && values.Length > _inParamsCount)
            {
                throw new DbMethodException(
                    string.Format("Too many values supplied for this command. The acceptable number of input values is {0}.", 
                    _inParamsCount.ToString()));
            }

            //
            // NOTE: Copying of command properties and parameters is done
            // here by-hand instead of cloning because it is much, much
            // faster (according to some tests, cloning was found to be 40
            // times slower!). Otherwise, it would have been nice if
            // one could have just written this code as simply:
            //
            // return (SqlCommand) ((ICloneable) _command).Clone();
            //

            SqlCommand newCommand = new SqlCommand(_command.CommandText, connection, transaction);
            newCommand.CommandType = _command.CommandType;

            for (int i = 0; i < _command.Parameters.Count; i++)
            {
                SqlParameter templateParameter = _command.Parameters[i];

                newCommand.Parameters.Add(new SqlParameter(
                    templateParameter.ParameterName, 
                    templateParameter.SqlDbType, 
                    templateParameter.Size, 
                    templateParameter.Direction, 
                    templateParameter.IsNullable, 
                    templateParameter.Precision, 
                    templateParameter.Scale, 
                    templateParameter.SourceColumn, 
                    templateParameter.SourceVersion, 
                    values != null && i < values.Length ? values[i] : null));
            }

            return newCommand;
        }

        IDbCommand IDbMethod.CreateCommand(IDbConnection connection, object[] values)
        {
            return CreateCommand((SqlConnection) connection, values);
        }

        private void Initialize()
        {
            Initialize((SqlMethodAttribute) Attribute.GetCustomAttribute(Method, 
                typeof(SqlMethodAttribute)));
        }

        private void Initialize(SqlMethodAttribute attribute)
        {
            Debug.Assert(attribute != null);

            if (attribute.IsCommandTextDefined == false && 
                attribute.CommandType == CommandType.StoredProcedure)
            {
                Initialize(attribute.CommandType, Method.Name);
            }
            else 
            {
                Initialize(attribute.CommandType, attribute.CommandText);
            }
        }

        private void Initialize(CommandType commandType, string commandText)
        {
            Debug.Assert(commandText != null);

            DbMethodReflector.ValidateInMethodSignature(Method);

            //
            // Remember the count of the input parameters to be expected.
            // This is used later during invocation to directly index
            // into the parameters collection rather than looking up 
            // parameters by name.
            //

            ParameterInfo[] parameters = Method.GetParameters();
            _inParamsCount = parameters.Length - 1;

            //
            // Find the partner output method and merge get an array
            // of parameters that logically combines the parameters
            // of both methods.
            //
            
            MethodInfo outMethod = DbMethodReflector.FindOutMethod(Method);
            DbMethodParameterInfo[] logicalParameters = DbMethodReflector.GetMergedParameters(Method, outMethod);

            //
            // Create the template command, configure it and then build all 
            // of its parameters.
            //

            _command = new SqlCommand(commandText);
            _command.CommandType = commandType;
            
            foreach (DbMethodParameterInfo logicalParameter in logicalParameters)
            {
                SqlParameter sqlParameter = CreateTemplateParameter(logicalParameter);
                _command.Parameters.Add(sqlParameter);
            }                
        }

        private static SqlParameter CreateTemplateParameter(DbMethodParameterInfo logicalParameter)
        {
            //
            // Get the SqlParameter attribute on the paramter. If none is
            // specified, then create one with defaults.
            //

            SqlParameterAttributeBase attribute = (SqlParameterAttributeBase) logicalParameter.GetDbParameterAttribute();

            if (attribute == null)
                attribute = new SqlParameterAttribute();

            //
            // Configure and configure the parameter's name and type.
            //

            SqlParameter sqlParameter = new SqlParameter();

            try
            {
                attribute.ConfigureParameter(sqlParameter, 
                    "@" + logicalParameter.Name,
                    logicalParameter.DirectType, 
                    logicalParameter.InferDirection());
            }
            catch (NotSupportedException)
            {
                MethodInfo method = (MethodInfo) (logicalParameter.InParameter != null ?
                    logicalParameter.InParameter.Member :
                    logicalParameter.OutParameter.Member);

                throw new DbMethodException(
                    string.Format("The {0} parameter of {1} in {2} cannot be used in a command. It is of type {3} that is not supported.",
                    logicalParameter.Name, method.DeclaringType.FullName, method.Name, 
                    logicalParameter.DirectType.FullName));
            }

            return sqlParameter;
        }

        /// <summary>
        /// Invokes the implementation as a logical call.
        /// </summary>
        /// <param name="call">
        /// The call state captured at the call site.
        /// </param>
        /// <remarks>
        /// This method is called internally by <see cref="LogicalMethodImpl"/>.
        /// </remarks>

        protected override void OnInvoke(LogicalCall call)
        {
            SqlCommand command = CreateCommand((SqlConnection) call.GetParameterValue(0));

            for (int i = 0; i < _inParamsCount; i++)
                command.Parameters[i].Value = call.GetParameterValue(i + 1);

            call.ReturnValue = command;
        }
    }
}
